### Load standardpackages
library(tidyverse) # Collection of all the good stuff like dplyr, ggplot2 ect.
library(magrittr) # For extra-piping operators (eg. %<>%)
library(tidytext)

This session

Refresher:

Bag of words model

  • In order for a computer to understand text we need to somehow find a useful representation.
  • If you need to compare different texts e.g. articles, you will probably go for keywords. These keywords may come from a keyword-list with for example 200 different keywords
  • In that case you could represent each document with a (sparse) vector with 1 for “keyword present” and 0 for “keyword absent”
  • We can also get a bit more sophoistocated and count the number of times a word from our dictionary occurs.
  • For a corpus of documents that would give us a document-term matrix.

example

Let’s try creating a bag of words model from our initial example.

text <- tibble(id = c(1:6),
               text = c('A text about cats.',
                        'A text about dogs.',
                        'And another text about a dog.',
                        'Why always writing about cats and dogs, always dogs?',
                        'There are too little text about cats but to many about dogs',
                        'Cats, cats, cats! I love cats soo much. Cats are way better than dogs'))
text_tidy <- text %>% 
  unnest_tokens(word, text, token = 'words') %>% 
  count(id, word)

The document-term matrix (DTM)

  • The simplest form of vector representation of text is a ddocument-term matrix
  • How to we get a document-term matrix now?
  • We could do it by hand, with well-known dplyr syntax (Note: only works when you have one row per unique document-word pair)
text_tidy %>%
  pivot_wider(names_from = word, values_from = n, values_fill = 0)
  • We could also use cast_dtm() to create a DTM in the format of the tm package.
text_dtm <- text_tidy %>%
  cast_dtm(id, word, n)
text_dtm 
<<DocumentTermMatrix (documents: 6, terms: 25)>>
Non-/sparse entries: 42/108
Sparsity           : 72%
Maximal term length: 7
Weighting          : term frequency (tf)
  • We can simply convert ig to a tibble. Since there exists no direct transfer function, we have to first transform it to a matrix.
  • Notice how we recover the rownames
text_dtm %>% as.matrix() %>% as_tibble(rownames = 'id') 
  • Sidenote: We can also tidy the DTM again to a tidy token-dataframe.
text_dtm %>% tidy()
  • We also can directly use a similar function to cast a sparse matrix (which we for sure then also could transform to a tibble again)
text_tidy %>% cast_sparse(row = id, column = word, value = n)
6 x 25 sparse Matrix of class "dgCMatrix"
                                                   
1 1 1 1 1 . . . . . . . . . . . . . . . . . . . . .
2 1 1 . 1 1 . . . . . . . . . . . . . . . . . . . .
3 1 1 . 1 . 1 1 1 . . . . . . . . . . . . . . . . .
4 . 1 1 . 2 1 . . 2 1 1 . . . . . . . . . . . . . .
5 . 2 1 1 1 . . . . . . 1 1 1 1 1 1 1 . . . . . . .
6 . . 5 . 1 . . . . . . 1 . . . . . . 1 1 1 1 1 1 1
  • Finally, we could just apply a text recipe here
library(recipes)
library(textrecipes)
text %>%
  recipe(~.) %>% 
  step_tokenize(text, token = 'words') %>% # tokenize
  step_tf(text) %>% # TFIDF weighting
  prep() %>% juice()

TF-IDF - Term Frequency - Inverse Document Frequency

  • A token is important for a document if appears very often
  • A token becomes less important for comparison across a corpus if it appears all over the place in the corpus
  • Cat in a corpus of websites talking about cats is not that important

\[w_{i,j} = tf_{i,j}*log(\frac{N}{df_i})\]

  • \(w_{i,j}\) = the TF-IDF score for a term i in a document j
  • \(tf_{i,j}\) = number of occurence of term i in document j
  • \(N\) = number of documents in the corpus
  • \(df_i\) = number of documents with term i
# TFIDF weights
text_tidy %<>%
  bind_tf_idf(term = word,
              document = id,
              n = n)
  • We obviously could also cast a tf_idf weighted dtm…
text_tidy %>%
  select(id, word, tf_idf) %>%
  pivot_wider(names_from = word, values_from = tf_idf, values_fill = 0)
  • btw: this is equivalent to just running a textrecipe like that:
text %>%
  recipe(~.) %>% 
  step_tokenize(text, token = 'words') %>% # tokenize
  step_tfidf(text) %>% # TFIDF weighting
  prep() %>% juice()
  • A last reminder on the powerful pairwise_xx() functions from the widyr package
  • For instance, pair
library(widyr)
text_tidy %>% pairwise_dist(id, word, tf_idf, method = "manhattan") %>%
  mutate(similarity = 1 - (distance / max(distance)) ) %>%
  select(-distance) %>%
  arrange(desc(similarity))

Dimensionality reduction techniques

rm(list=ls())
  • Ok, lets get first some more interesting data. We will work with the CORDIS project descriptions of EU Horizon 2020 projects again.
text <- read_csv('https://github.com/SDS-AAU/SDS-master/raw/master/M2/data/cordis-h2020reports.gz')
colnames(text) <- colnames(text) %>% str_to_lower()
text %<>%
  select(-x1) %>%
  rename(id = projectid) %>%
  relocate(id) %>%
  filter(language == 'en') %>%
  drop_na(id)
  • Lets create a tidy tokenlist
text_tidy <- text %>%
  rename(text = summary) %>%
  select(id, text) %>%
  unnest_tokens(word, text, token = "words")
  • some preprocessing
# preprocessing
text_tidy %<>%
  filter(str_length(word) > 2 ) %>% # Remove words with less than  3 characters
  filter(!(word %in% c('project', 'research'))) %>%
  anti_join(stop_words, by = 'word') 
  • We can also ad bigrams
text_tidy %<>%
  unnest_tokens(word, word, token = 'ngrams', n = 2, n_min = 1) %>%
  group_by(word) %>% filter(n() > 25) %>% ungroup() 
text_tidy %>%
  count(word, sort = TRUE)
  • Lets finish this up and also add TF-IDF weights
text_tidy %<>%
  count(id, word) %>%
  bind_tf_idf(term = word,
              document = id,
              n = n) %>%
  select(-tf, -idf)
  • Is there a big difference?
text_tidy %>%
  count(word, wt = tf_idf, sort = TRUE)
  • And finally, lets get a DTM dataframe
text_dtm <- text_tidy %>%
  select(id, word, n) %>%
  pivot_wider(names_from = word, values_from = n, values_fill = 0)
  • And, just in case, a TFIDF weighted version
  • We could also prepare a recipe which doe pretty much the same…
recipe_base <- text %>%
  rename(text = summary) %>%
  select(id, text) %>%
  # BAse recipe starts
  recipe(~.) %>% 
  update_role(id, new_role = "id") %>% # Update role of ID
  step_tokenize(text, token = 'words') %>% # tokenize
  step_stopwords(text, keep = FALSE) %>% # remove stopwords
  step_untokenize(text) %>% # Here we now have to first untokenize
  step_tokenize(text, token = "ngrams", options = list(n = 1, n_min = 1)) %>% # and tokenize again
  step_tokenfilter(text, min_times = 25) 
  • Here, we can further preprocess to do whatever we would like, such as obtaining a dtm
base_recipe %>% 
  step_tf(text) %>% 
  prep() %>% 
  juice() %>% 
  head(100)
text_pca <- text_dtm %>% 
  column_to_rownames('id') %>% 
  prcomp(center = TRUE, scale. = TRUE, rank. = 10)
text_pca %>% glimpse()
List of 5
 $ sdev    : num [1:499] 3.58 3.11 2.97 2.85 2.71 ...
 $ rotation: num [1:608, 1:10] 0.01761 0.00292 0.07104 -0.03197 0.01753 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : chr [1:608] "aim" "allowing" "based" "blood" ...
  .. ..$ : chr [1:10] "PC1" "PC2" "PC3" "PC4" ...
 $ center  : Named num [1:608] 0.2265 0.0541 0.6733 0.0701 0.1543 ...
  ..- attr(*, "names")= chr [1:608] "aim" "allowing" "based" "blood" ...
 $ scale   : Named num [1:608] 0.537 0.235 1.049 0.445 0.856 ...
  ..- attr(*, "names")= chr [1:608] "aim" "allowing" "based" "blood" ...
 $ x       : num [1:499, 1:10] -3.259 -0.996 -1.711 -1.379 -1.575 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : chr [1:499] "115844" "633197" "633249" "633261" ...
  .. ..$ : chr [1:10] "PC1" "PC2" "PC3" "PC4" ...
 - attr(*, "class")= chr "prcomp"
text_pca[['x']] %>%
  head()
              PC1        PC2       PC3        PC4        PC5        PC6        PC7        PC8        PC9       PC10
115844 -3.2588756 -0.8478672  1.286494 -0.3304838  0.6253670 -0.3161002  0.1642597 -2.2037321 -0.1871126  0.8770474
633197 -0.9960611  4.4346346 -1.054370 -2.9036039 -1.4704782 -0.9094432 -1.6293613 -1.6208713 -0.2130936  2.4375617
633249 -1.7111795  3.7095798 -2.546628 -2.6489614 -2.1026976 -0.7091236  0.6661537 -0.1671077  0.3804010  1.2127379
633261 -1.3789058  4.1268532 -2.175831 -4.1895254 -0.8737219 -1.0295514 -1.1417048 -1.2886798 -1.7668852  2.6082576
633382 -1.5749243  4.2602715 -3.418563 -3.7036367 -1.1608198 -1.0926355 -1.1411842  0.2951679 -0.2694360  1.6113388
633571  1.2576733  1.6711741 -2.251064 -0.9706029 -1.5562738  0.6804761 -0.2523918 -0.2671309  0.9906243 -3.8692253
  • Again, alternatively with a recipe…
recipe_pca <- recipe_base %>% # tokenize
  step_tfidf(text, prefix = '') %>% # TFIDF weighting
  step_pca(all_predictors(), num_comp = 10) %>% # PCA
  prep() 
recipe_pca %>% juice()
  • Some plotting
recipe_pca %>% juice() %>%
  ggplot(aes(x = PC01, y = PC02)) +
  geom_point() 

  • we can also use the tidy results of the recipe to do some more analytics
recipe_pca %>%
  tidy(7) %>%
  filter(component %in% paste0("PC", 1:4)) %>%
  group_by(component) %>%
    arrange(desc(value)) %>%
    slice(c(1:2, (n()-2):n())) %>%
  ungroup() %>%
  mutate(component = fct_inorder(component)) %>%
  ggplot(aes(value, terms, fill = terms)) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~component, nrow = 1) +
  labs(y = NULL)

  • Note: Also check further for further dimensionlity reduction steps:
    • tep_kpca():
    • step_ica()
    • step_isomap()
    • step_nmf()

Topic Models: Latent-Dirichlet-Allocation (LDA)

  • While we already did it somewhat ‘on-the-fly’, here a more formal introduction to LDA
  • In contrast to dimnesionality reduction techiques mostly aiming at preprocessing data or easing visualization, LDA more aims at EDA and interpretation
  • It is a generative approach to identify topics (clusters) within the word-usage in documents.
    • Topics are represented as a probability distribution over the words in the vocabulary. Hhigh probability words can be used to charactrize the topic.
    • Documents are represented as a mixture of topics.

alt text

library(topicmodels)
text_dtm
<<DocumentTermMatrix (documents: 499, terms: 608)>>
Non-/sparse entries: 24900/278492
Sparsity           : 92%
Maximal term length: 21
Weighting          : term frequency (tf)
text_lda <- text_dtm %>% 
  LDA(k = 6, method = "Gibbs",
      control = list(seed = 1337))
  • \(\beta\) is an output of the LDA model, indicating the propability that a word occurs in a certain topic.
  • Therefore, loking at the top probability words of a topic often gives us a good intuition regarding its properties.
# LDA output is defined for tidy(), so we can easily extract it
lda_beta <- text_lda %>% 
  tidy(matrix = "beta") 
lda_beta %>%
  # slice
  group_by(topic) %>%
  arrange(topic, desc(beta)) %>%
  slice(1:10) %>%
  ungroup() %>%
  # visualize
  mutate(term = reorder_within(term, beta, topic)) %>%
  group_by(topic, term) %>%    
  arrange(desc(beta)) %>%  
  ungroup() %>%
  ggplot(aes(term, beta, fill = as.factor(topic))) +
  geom_col(show.legend = FALSE) +
  coord_flip() +
  scale_x_reordered() +
  labs(title = "Top 10 terms in each LDA topic",
       x = NULL, y = expression(beta)) +
  facet_wrap(~ topic, ncol = 3, scales = "free")

  • Documents are represented as a mix of topics. This association of a document to a topic is captured by \(\gamma\)
lda_gamma %>%
  group_by(topic) %>%
  arrange(desc(gamma)) %>% 
  slice(1:10) %>%
    ungroup() %>%
  left_join(text %>% select(id, projectacronym) %>% mutate(id = id %>% as.character()), by = c('document' = 'id'))
  • Note that an LDA can also be performed via a recipe:
recipe_lda <- recipe_base %>% # tokenize
  step_untokenize(text) %>% # Is a bit silly, needs the full text vectors instead of tokens....
  step_lda(text, num_topics = 6) %>% # LDA
  prep() 
recipe_lda %>% juice() %>% 
  head(100)
  • As a bonus, a great way to interactively visualize LDA’s.
  • It’s a bit cumbersome in R, though…
library(LDAvis)
topicmodels_json_ldavis <- function(fitted, doc_dtm, method = "PCA"){
  require(topicmodels); require(dplyr); require(LDAvis)
  
  # Find required quantities
  phi <- posterior(text_lda)$terms %>% as.matrix() # Topic-term distribution
  theta <- posterior(fitted)$topics %>% as.matrix() # Document-topic matrix
  
  text_tidy <- doc_dtm %>% tidy()
  vocab <- colnames(phi)
  doc_length <- tibble(document = rownames(theta)) %>% left_join(text_tidy %>% count(document, wt = count), by = 'document')
  tf <- tibble(term = vocab) %>% left_join(text_tidy %>% count(term, wt = count), by = "term") 
  
  if(method == "PCA"){mds <- jsPCA}
  if(method == "TSNE"){library(tsne); mds <- function(x){tsne(svd(x)$u)} }
  
  # Convert to json
  json_lda <- LDAvis::createJSON(phi = phi, theta = theta, vocab = vocab, doc.length = doc_length %>% pull(n), term.frequency = tf %>% pull(n),
                                 reorder.topics = FALSE, mds.method = mds,plot.opts = list(xlab = "Dim.1", ylab = "Dim.2")) 
  return(json_lda)
}
library(LDAvis)
json_lda <- topicmodels_json_ldavis(fitted = text_lda, 
                                    doc_dtm = text_dtm, 
                                    method = "TSNE")

json_lda %>% serVis(out.dir = 'LDAviz')

Didnt really figure out how to embedd the resulting plot, but the outcome can be seen here

Embeddings (Bonus)

  • One last thing we did not venture in yet, are embeddings

  • I will not go into details here, just see it as a peak of what’s to come in further sessions.

  • The idee of word embedding is (in a nutshell) that

  • There are packages on how to train own embeddings such as text2vec, but we will for now not bother with that.

  • The only thing we will do for now is to load pretrained embeddings (GloVe, cf. Pennington et al, 2014)

library(textdata)

glove6b <- embedding_glove6b(dimensions = 100)
glove6b
# These mebeddings can now be loaded with step_wordembeddings
  • La voila, a large pretrained embedding model for around 400k of the most common words.
  • We for now loaded the smallest of these embedding models, there exist way bigger ones.
  • Lets join it with our tidy tokenlist
word_embeddings <- text_tidy %>%
  inner_join(glove6b, by = c('word' = 'token'))
word_embeddings %>% head()
  • We could now create average document embeddings by taking the mean over all dimensions
  • We could also (even better) weight that by then word’s tfidf score.
  • These embddings could now be used for instance for some clustering or SML exercise
  • I guess you can already see how to use these embeddings in an SML model.
library(uwot) # for UMAP
embeddings_umap <- doc_embeddings  %>% 
  column_to_rownames("id") %>%
  umap(n_neighbors = 15, 
       metric = "cosine", 
       min_dist = 0.01, 
       scale = TRUE,
       verbose = TRUE, 
       n_threads = 8) 
0%   10   20   30   40   50   60   70   80   90   100%
[----|----|----|----|----|----|----|----|----|----|
**************************************************|
embeddings_umap %<>% as.data.frame()
embeddings_umap  %>% 
  ggplot(aes(x = V1, y = V2)) + 
  geom_point(shape = 21, alpha = 0.5) 

  • Ok, we see a rather clear seperation of documents.
  • Just for fun, lets add a density based clustering (very good for spatial clustering) on top (even though we already see the results)
library(dbscan)
  • Do the hirarchical density based clustering
embeddings_hdbscan <- embeddings_umap %>% as.matrix() %>% hdbscan(minPts = 15)
  • Plot it
embeddings_umap %>% 
  bind_cols(cluster = embeddings_hdbscan$cluster %>% as.factor(), 
            prob = embeddings_hdbscan$membership_prob) %>%
  ggplot(aes(x = V1, y = V2, col = cluster)) + 
  geom_point(aes(alpha = prob), shape = 21) 

  • Note: We can also assigne the embeddings via a recipe
  • Unfortunately, we can not do a TFIDF weighting here ‘out-of-the-box’, but have to work with average embeddings instead.
recipe_embedding <- recipe_base %>% # tokenize
  step_word_embeddings(text, embeddings = glove6b, aggregation = 'mean')
recipe_embedding %>% prep() %>% juice() %>% 
  head(100)
  • Same goes for UMAP, which can be accessd in recipes via the the package embed pckage.
  • However,embed is a bit heavy in terms of dependencies, since it uses keras and tensorflow, a deep learning framewok, in the backgroubnd, and is in need to install another mini-conda enviroment.
  • If you have no experience with keras and tensorflow so far, I suggest you wait with this one until later sessions when we properly introduce it.
library(embed)
recipe_umap <- recipe_embedding %>%
  step_umap(starts_with('w_embed'), n_neighbors = 15) 
recipe_umap %>% prep() %>% juice() %>% 
  head(100)
  • So, that’s all I have for now

Summary

  • There are many ways to convert text data into a vector representation.
  • These range from simple and weighted bags-of-words, to topic models, over different types of dimensionality reduction to finally word and document embeddings.
  • All of them are useful, depending on the purpose.

Endnotes

Packages & Ecosystem

  • textrecipes: Text preprocessing recipes
  • embed: Extra embedding recipes
  • topicmodels: LDA topicmodelling in R
  • LDAvis: A bit clunky but awesome interactive LDA visualizations
  • text2vec: Package vor vector space modelling (aka embeddings & other vectorizations) of textdata
  • textdata: Useful datasets for text, such as GloVe embeddings, sentiment lexica etc.
  • uwot: UMAP for R
  • uwot: UMAP for R

References

CHapters:

  • Julia Silge and David Robinson (2020). Text Mining with R: A Tidy Approach, O’Reilly. Online available here
  • Emil Hvidfeldt and Julia Silge (2020). Supervised Machine Learning for Text Analysis in R, online available here

Articles: * Blei, David M., Andrew Y. Ng, and Michael I. Jordan. “Latent dirichlet allocation.” Journal of machine Learning research 3, no. Jan (2003): 993-1022. * Jeffrey Pennington, Richard Socher, and Christopher D Manning. Glove: Global vectors for word representation. In Conference on Empirical Methods on Natural Language Processing (EMNLP), pages 1532–1543, 2014

Further sources

Session Info

sessionInfo()
LS0tCnRpdGxlOiAnKFNvbWV3aGF0KSBhZHZhbmNlZCBOTFA6IHRleHQgdmVjdG9yaXphdGlvbicKYXV0aG9yOiAiRGFuaWVsIFMuIEhhaW4gKGRzaEBidXNpbmVzcy5hYXUuZGspIgpkYXRlOiAiVXBkYXRlZCBgciBmb3JtYXQoU3lzLnRpbWUoKSwgJyVCICVkLCAlWScpYCIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6CiAgICBjb2RlX2ZvbGRpbmc6IHNob3cKICAgIGRmX3ByaW50OiBwYWdlZAogICAgdG9jOiB0cnVlCiAgICB0b2NfZGVwdGg6IDIKICAgIHRvY19mbG9hdDoKICAgICAgY29sbGFwc2VkOiBmYWxzZQogICAgdGhlbWU6IGZsYXRseQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQojIyMgR2VuZXJpYyBwcmVhbWJsZQpybShsaXN0PWxzKCkpClN5cy5zZXRlbnYoTEFORyA9ICJlbiIpICMgRm9yIGVuZ2xpc2ggbGFuZ3VhZ2UKb3B0aW9ucyhzY2lwZW4gPSA1KSAjIFRvIGRlYWN0aXZhdGUgYW5ub3lpbmcgc2NpZW50aWZpYyBudW1iZXIgbm90YXRpb24KCiMjIyBLbml0ciBvcHRpb25zCmxpYnJhcnkoa25pdHIpICMgRm9yIGRpc3BsYXkgb2YgdGhlIG1hcmtkb3duCmtuaXRyOjpvcHRzX2NodW5rJHNldCh3YXJuaW5nPUZBTFNFLAogICAgICAgICAgICAgICAgICAgICBtZXNzYWdlPUZBTFNFLAogICAgICAgICAgICAgICAgICAgICBjb21tZW50PUZBTFNFLCAKICAgICAgICAgICAgICAgICAgICAgZmlnLmFsaWduPSJjZW50ZXIiCiAgICAgICAgICAgICAgICAgICAgICkKYGBgCgpgYGB7cn0KIyMjIExvYWQgc3RhbmRhcmRwYWNrYWdlcwpsaWJyYXJ5KHRpZHl2ZXJzZSkgIyBDb2xsZWN0aW9uIG9mIGFsbCB0aGUgZ29vZCBzdHVmZiBsaWtlIGRwbHlyLCBnZ3Bsb3QyIGVjdC4KbGlicmFyeShtYWdyaXR0cikgIyBGb3IgZXh0cmEtcGlwaW5nIG9wZXJhdG9ycyAoZWcuICU8PiUpCmBgYAoKYGBge3J9CmxpYnJhcnkodGlkeXRleHQpCmBgYAoKIyBUaGlzIHNlc3Npb24KCgojIFJlZnJlc2hlcjoKCiFbXShodHRwczovL3Nkcy1hYXUuZ2l0aHViLmlvL1NEUy1tYXN0ZXIvMDBfbWVkaWEvbmxwX3RpZHl3b3JrZmxvdy5wbmcpCgoKIyBCYWcgb2Ygd29yZHMgbW9kZWwKCiogSW4gb3JkZXIgZm9yIGEgY29tcHV0ZXIgdG8gdW5kZXJzdGFuZCB0ZXh0IHdlIG5lZWQgdG8gc29tZWhvdyBmaW5kIGEgdXNlZnVsIHJlcHJlc2VudGF0aW9uLgoqIElmIHlvdSBuZWVkIHRvIGNvbXBhcmUgZGlmZmVyZW50IHRleHRzIGUuZy4gYXJ0aWNsZXMsIHlvdSB3aWxsIHByb2JhYmx5IGdvIGZvciBrZXl3b3Jkcy4gVGhlc2Uga2V5d29yZHMgbWF5IGNvbWUgZnJvbSBhIGtleXdvcmQtbGlzdCB3aXRoIGZvciBleGFtcGxlIDIwMCBkaWZmZXJlbnQga2V5d29yZHMKKiBJbiB0aGF0IGNhc2UgeW91IGNvdWxkIHJlcHJlc2VudCBlYWNoIGRvY3VtZW50IHdpdGggYSAoc3BhcnNlKSB2ZWN0b3Igd2l0aCAxIGZvciAia2V5d29yZCBwcmVzZW50IiBhbmQgMCBmb3IgImtleXdvcmQgYWJzZW50IgoqIFdlIGNhbiBhbHNvIGdldCBhIGJpdCBtb3JlIHNvcGhvaXN0b2NhdGVkIGFuZCBjb3VudCB0aGUgbnVtYmVyIG9mIHRpbWVzIGEgd29yZCBmcm9tIG91ciBkaWN0aW9uYXJ5IG9jY3Vycy4KKiBGb3IgYSBjb3JwdXMgb2YgZG9jdW1lbnRzIHRoYXQgd291bGQgZ2l2ZSB1cyBhIGRvY3VtZW50LXRlcm0gbWF0cml4LgoKIVtleGFtcGxlXShodHRwczovL2kuc3RhY2suaW1ndXIuY29tL0MxVU1zLnBuZykKCkxldCdzIHRyeSBjcmVhdGluZyBhIGJhZyBvZiB3b3JkcyBtb2RlbCBmcm9tIG91ciBpbml0aWFsIGV4YW1wbGUuCgpgYGB7cn0KdGV4dCA8LSB0aWJibGUoaWQgPSBjKDE6NiksCiAgICAgICAgICAgICAgIHRleHQgPSBjKCdBIHRleHQgYWJvdXQgY2F0cy4nLAogICAgICAgICAgICAgICAgICAgICAgICAnQSB0ZXh0IGFib3V0IGRvZ3MuJywKICAgICAgICAgICAgICAgICAgICAgICAgJ0FuZCBhbm90aGVyIHRleHQgYWJvdXQgYSBkb2cuJywKICAgICAgICAgICAgICAgICAgICAgICAgJ1doeSBhbHdheXMgd3JpdGluZyBhYm91dCBjYXRzIGFuZCBkb2dzLCBhbHdheXMgZG9ncz8nLAogICAgICAgICAgICAgICAgICAgICAgICAnVGhlcmUgYXJlIHRvbyBsaXR0bGUgdGV4dCBhYm91dCBjYXRzIGJ1dCB0byBtYW55IGFib3V0IGRvZ3MnLAogICAgICAgICAgICAgICAgICAgICAgICAnQ2F0cywgY2F0cywgY2F0cyEgSSBsb3ZlIGNhdHMgc29vIG11Y2guIENhdHMgYXJlIHdheSBiZXR0ZXIgdGhhbiBkb2dzJykpCmBgYAoKYGBge3J9CnRleHRfdGlkeSA8LSB0ZXh0ICU+JSAKICB1bm5lc3RfdG9rZW5zKHdvcmQsIHRleHQsIHRva2VuID0gJ3dvcmRzJykgJT4lIAogIGNvdW50KGlkLCB3b3JkKQpgYGAKCgojIyBUaGUgZG9jdW1lbnQtdGVybSBtYXRyaXggKERUTSkKCiogVGhlIHNpbXBsZXN0IGZvcm0gb2YgdmVjdG9yIHJlcHJlc2VudGF0aW9uIG9mIHRleHQgaXMgYSBkZG9jdW1lbnQtdGVybSBtYXRyaXgKKiBIb3cgdG8gd2UgZ2V0IGEgZG9jdW1lbnQtdGVybSBtYXRyaXggbm93PwoqIFdlIGNvdWxkIGRvIGl0IGJ5IGhhbmQsIHdpdGggd2VsbC1rbm93biBgZHBseXJgIHN5bnRheCAoTm90ZTogb25seSB3b3JrcyB3aGVuIHlvdSBoYXZlIG9uZSByb3cgcGVyIHVuaXF1ZSBkb2N1bWVudC13b3JkIHBhaXIpCgpgYGB7cn0KdGV4dF90aWR5ICU+JQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSB3b3JkLCB2YWx1ZXNfZnJvbSA9IG4sIHZhbHVlc19maWxsID0gMCkKYGBgCgoqIFdlIGNvdWxkIGFsc28gdXNlIGBjYXN0X2R0bSgpYCB0byBjcmVhdGUgYSBEVE0gaW4gdGhlIGZvcm1hdCBvZiB0aGUgYHRtYCBwYWNrYWdlLgoKYGBge3J9CnRleHRfZHRtIDwtIHRleHRfdGlkeSAlPiUKICBjYXN0X2R0bShpZCwgd29yZCwgbikKYGBgCgpgYGB7cn0KdGV4dF9kdG0gCmBgYAoKKiBXZSBjYW4gc2ltcGx5IGNvbnZlcnQgaWcgdG8gYSB0aWJibGUuIFNpbmNlIHRoZXJlIGV4aXN0cyBubyBkaXJlY3QgdHJhbnNmZXIgZnVuY3Rpb24sIHdlIGhhdmUgdG8gZmlyc3QgdHJhbnNmb3JtIGl0IHRvIGEgbWF0cml4LgoqIE5vdGljZSBob3cgd2UgcmVjb3ZlciB0aGUgcm93bmFtZXMKCmBgYHtyfQp0ZXh0X2R0bSAlPiUgYXMubWF0cml4KCkgJT4lIGFzX3RpYmJsZShyb3duYW1lcyA9ICdpZCcpIApgYGAKCiogU2lkZW5vdGU6IFdlIGNhbiBhbHNvIHRpZHkgdGhlIERUTSBhZ2FpbiB0byBhIHRpZHkgdG9rZW4tZGF0YWZyYW1lLgoKYGBge3J9CnRleHRfZHRtICU+JSB0aWR5KCkKYGBgCiogV2UgYWxzbyBjYW4gZGlyZWN0bHkgdXNlIGEgc2ltaWxhciBmdW5jdGlvbiB0byBjYXN0IGEgc3BhcnNlIG1hdHJpeCAod2hpY2ggd2UgZm9yIHN1cmUgdGhlbiBhbHNvIGNvdWxkIHRyYW5zZm9ybSB0byBhIHRpYmJsZSBhZ2FpbikKCmBgYHtyfQp0ZXh0X3RpZHkgJT4lIGNhc3Rfc3BhcnNlKHJvdyA9IGlkLCBjb2x1bW4gPSB3b3JkLCB2YWx1ZSA9IG4pCmBgYAoKKiBGaW5hbGx5LCB3ZSBjb3VsZCBqdXN0IGFwcGx5IGEgdGV4dCByZWNpcGUgaGVyZQoKYGBge3J9CmxpYnJhcnkocmVjaXBlcykKbGlicmFyeSh0ZXh0cmVjaXBlcykKYGBgCgpgYGB7cn0KdGV4dCAlPiUKICByZWNpcGUofi4pICU+JSAKICBzdGVwX3Rva2VuaXplKHRleHQsIHRva2VuID0gJ3dvcmRzJykgJT4lICMgdG9rZW5pemUKICBzdGVwX3RmKHRleHQpICU+JSAjIFRGSURGIHdlaWdodGluZwogIHByZXAoKSAlPiUganVpY2UoKQpgYGAKCgojIyBURi1JREYgLSBUZXJtIEZyZXF1ZW5jeSAtIEludmVyc2UgRG9jdW1lbnQgRnJlcXVlbmN5CgoqIEEgdG9rZW4gaXMgaW1wb3J0YW50IGZvciBhIGRvY3VtZW50IGlmIGFwcGVhcnMgdmVyeSBvZnRlbgoqIEEgdG9rZW4gYmVjb21lcyBsZXNzIGltcG9ydGFudCBmb3IgY29tcGFyaXNvbiBhY3Jvc3MgYSBjb3JwdXMgaWYgaXQgYXBwZWFycyBhbGwgb3ZlciB0aGUgcGxhY2UgaW4gdGhlIGNvcnB1cwoqICpDYXQqIGluIGEgY29ycHVzIG9mIHdlYnNpdGVzIHRhbGtpbmcgYWJvdXQgY2F0cyBpcyBub3QgdGhhdCBpbXBvcnRhbnQKCiQkd197aSxqfSA9IHRmX3tpLGp9KmxvZyhcZnJhY3tOfXtkZl9pfSkkJAoKLSAkd197aSxqfSQgPSB0aGUgVEYtSURGIHNjb3JlIGZvciBhIHRlcm0gaSBpbiBhIGRvY3VtZW50IGoKLSAkdGZfe2ksan0kID0gbnVtYmVyIG9mIG9jY3VyZW5jZSBvZiB0ZXJtIGkgaW4gZG9jdW1lbnQgagotICROJCA9IG51bWJlciBvZiBkb2N1bWVudHMgaW4gdGhlIGNvcnB1cwotICRkZl9pJCA9IG51bWJlciBvZiBkb2N1bWVudHMgd2l0aCB0ZXJtIGkKCmBgYHtyfQojIFRGSURGIHdlaWdodHMKdGV4dF90aWR5ICU8PiUKICBiaW5kX3RmX2lkZih0ZXJtID0gd29yZCwKICAgICAgICAgICAgICBkb2N1bWVudCA9IGlkLAogICAgICAgICAgICAgIG4gPSBuKQpgYGAKCiogV2Ugb2J2aW91c2x5IGNvdWxkIGFsc28gY2FzdCBhIHRmX2lkZiB3ZWlnaHRlZCBkdG0uLi4KCmBgYHtyfQp0ZXh0X3RpZHkgJT4lCiAgc2VsZWN0KGlkLCB3b3JkLCB0Zl9pZGYpICU+JQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSB3b3JkLCB2YWx1ZXNfZnJvbSA9IHRmX2lkZiwgdmFsdWVzX2ZpbGwgPSAwKQpgYGAKCiogYnR3OiB0aGlzIGlzIGVxdWl2YWxlbnQgdG8ganVzdCBydW5uaW5nIGEgdGV4dHJlY2lwZSBsaWtlIHRoYXQ6CgpgYGB7cn0KdGV4dCAlPiUKICByZWNpcGUofi4pICU+JSAKICBzdGVwX3Rva2VuaXplKHRleHQsIHRva2VuID0gJ3dvcmRzJykgJT4lICMgdG9rZW5pemUKICBzdGVwX3RmaWRmKHRleHQpICU+JSAjIFRGSURGIHdlaWdodGluZwogIHByZXAoKSAlPiUganVpY2UoKQpgYGAKCiogQSBsYXN0IHJlbWluZGVyIG9uIHRoZSBwb3dlcmZ1bCBgcGFpcndpc2VfeHgoKWAgZnVuY3Rpb25zIGZyb20gdGhlIGB3aWR5cmAgcGFja2FnZQoqIEZvciBpbnN0YW5jZSwgcGFpcgoKYGBge3J9CmxpYnJhcnkod2lkeXIpCmBgYAoKYGBge3J9CnRleHRfdGlkeSAlPiUgcGFpcndpc2VfZGlzdChpZCwgd29yZCwgdGZfaWRmLCBtZXRob2QgPSAibWFuaGF0dGFuIikgJT4lCiAgbXV0YXRlKHNpbWlsYXJpdHkgPSAxIC0gKGRpc3RhbmNlIC8gbWF4KGRpc3RhbmNlKSkgKSAlPiUKICBzZWxlY3QoLWRpc3RhbmNlKSAlPiUKICBhcnJhbmdlKGRlc2Moc2ltaWxhcml0eSkpCmBgYAoKCgojIERpbWVuc2lvbmFsaXR5IHJlZHVjdGlvbiB0ZWNobmlxdWVzCgpgYGB7cn0Kcm0obGlzdD1scygpKQpgYGAKCiogT2ssIGxldHMgZ2V0IGZpcnN0IHNvbWUgbW9yZSBpbnRlcmVzdGluZyBkYXRhLiBXZSB3aWxsIHdvcmsgd2l0aCB0aGUgQ09SRElTIHByb2plY3QgZGVzY3JpcHRpb25zIG9mIEVVIEhvcml6b24gMjAyMCBwcm9qZWN0cyBhZ2Fpbi4KCmBgYHtyfQp0ZXh0IDwtIHJlYWRfY3N2KCdodHRwczovL2dpdGh1Yi5jb20vU0RTLUFBVS9TRFMtbWFzdGVyL3Jhdy9tYXN0ZXIvTTIvZGF0YS9jb3JkaXMtaDIwMjByZXBvcnRzLmd6JykKYGBgCgpgYGB7cn0KY29sbmFtZXModGV4dCkgPC0gY29sbmFtZXModGV4dCkgJT4lIHN0cl90b19sb3dlcigpCnRleHQgJTw+JQogIHNlbGVjdCgteDEpICU+JQogIHJlbmFtZShpZCA9IHByb2plY3RpZCkgJT4lCiAgcmVsb2NhdGUoaWQpICU+JQogIGZpbHRlcihsYW5ndWFnZSA9PSAnZW4nKSAlPiUKICBkcm9wX25hKGlkKQpgYGAKCiogTGV0cyBjcmVhdGUgYSB0aWR5IHRva2VubGlzdAoKYGBge3J9CnRleHRfdGlkeSA8LSB0ZXh0ICU+JQogIHJlbmFtZSh0ZXh0ID0gc3VtbWFyeSkgJT4lCiAgc2VsZWN0KGlkLCB0ZXh0KSAlPiUKICB1bm5lc3RfdG9rZW5zKHdvcmQsIHRleHQsIHRva2VuID0gIndvcmRzIikKYGBgCgoqIHNvbWUgcHJlcHJvY2Vzc2luZwoKYGBge3J9CiMgcHJlcHJvY2Vzc2luZwp0ZXh0X3RpZHkgJTw+JQogIGZpbHRlcihzdHJfbGVuZ3RoKHdvcmQpID4gMiApICU+JSAjIFJlbW92ZSB3b3JkcyB3aXRoIGxlc3MgdGhhbiAgMyBjaGFyYWN0ZXJzCiAgZmlsdGVyKCEod29yZCAlaW4lIGMoJ3Byb2plY3QnLCAncmVzZWFyY2gnKSkpICU+JQogIGFudGlfam9pbihzdG9wX3dvcmRzLCBieSA9ICd3b3JkJykgCmBgYAoKKiBXZSBjYW4gYWxzbyBhZCBiaWdyYW1zCgpgYGB7cn0KdGV4dF90aWR5ICU8PiUKICB1bm5lc3RfdG9rZW5zKHdvcmQsIHdvcmQsIHRva2VuID0gJ25ncmFtcycsIG4gPSAyLCBuX21pbiA9IDEpICU+JQogIGdyb3VwX2J5KHdvcmQpICU+JSBmaWx0ZXIobigpID4gMjUpICU+JSB1bmdyb3VwKCkgCmBgYAoKYGBge3J9CnRleHRfdGlkeSAlPiUKICBjb3VudCh3b3JkLCBzb3J0ID0gVFJVRSkKYGBgCgoqIExldHMgZmluaXNoIHRoaXMgdXAgYW5kIGFsc28gYWRkIFRGLUlERiB3ZWlnaHRzCgpgYGB7cn0KdGV4dF90aWR5ICU8PiUKICBjb3VudChpZCwgd29yZCkgJT4lCiAgYmluZF90Zl9pZGYodGVybSA9IHdvcmQsCiAgICAgICAgICAgICAgZG9jdW1lbnQgPSBpZCwKICAgICAgICAgICAgICBuID0gbikgJT4lCiAgc2VsZWN0KC10ZiwgLWlkZikKYGBgCgoqIElzIHRoZXJlIGEgYmlnIGRpZmZlcmVuY2U/CgpgYGB7cn0KdGV4dF90aWR5ICU+JQogIGNvdW50KHdvcmQsIHd0ID0gdGZfaWRmLCBzb3J0ID0gVFJVRSkKYGBgCgoqIEFuZCBmaW5hbGx5LCBsZXRzIGdldCBhIERUTSBkYXRhZnJhbWUgCgpgYGB7cn0KdGV4dF9kdG0gPC0gdGV4dF90aWR5ICU+JQogIHNlbGVjdChpZCwgd29yZCwgbikgJT4lCiAgcGl2b3Rfd2lkZXIobmFtZXNfZnJvbSA9IHdvcmQsIHZhbHVlc19mcm9tID0gbiwgdmFsdWVzX2ZpbGwgPSAwKQpgYGAKCiogQW5kLCBqdXN0IGluIGNhc2UsIGEgVEZJREYgd2VpZ2h0ZWQgdmVyc2lvbgoKYGBge3IsIGluY2x1ZGU9RkFMU0V9CnRleHRfZHRtX3RmX2lkZiA8LSB0ZXh0X3RpZHkgJT4lCiAgc2VsZWN0KGlkLCB3b3JkLCB0Zl9pZGYpICU+JQogIHBpdm90X3dpZGVyKG5hbWVzX2Zyb20gPSB3b3JkLCB2YWx1ZXNfZnJvbSA9IHRmX2lkZiwgdmFsdWVzX2ZpbGwgPSAwKQpgYGAKCiogV2UgY291bGQgYWxzbyBwcmVwYXJlIGEgcmVjaXBlIHdoaWNoIGRvZSBwcmV0dHkgbXVjaCB0aGUgc2FtZS4uLgoKYGBge3J9CnJlY2lwZV9iYXNlIDwtIHRleHQgJT4lCiAgcmVuYW1lKHRleHQgPSBzdW1tYXJ5KSAlPiUKICBzZWxlY3QoaWQsIHRleHQpICU+JQogICMgQkFzZSByZWNpcGUgc3RhcnRzCiAgcmVjaXBlKH4uKSAlPiUgCiAgdXBkYXRlX3JvbGUoaWQsIG5ld19yb2xlID0gImlkIikgJT4lICMgVXBkYXRlIHJvbGUgb2YgSUQKICBzdGVwX3Rva2VuaXplKHRleHQsIHRva2VuID0gJ3dvcmRzJykgJT4lICMgdG9rZW5pemUKICBzdGVwX3N0b3B3b3Jkcyh0ZXh0LCBrZWVwID0gRkFMU0UpICU+JSAjIHJlbW92ZSBzdG9wd29yZHMKICBzdGVwX3VudG9rZW5pemUodGV4dCkgJT4lICMgSGVyZSB3ZSBub3cgaGF2ZSB0byBmaXJzdCB1bnRva2VuaXplCiAgc3RlcF90b2tlbml6ZSh0ZXh0LCB0b2tlbiA9ICJuZ3JhbXMiLCBvcHRpb25zID0gbGlzdChuID0gMSwgbl9taW4gPSAxKSkgJT4lICMgYW5kIHRva2VuaXplIGFnYWluCiAgc3RlcF90b2tlbmZpbHRlcih0ZXh0LCBtaW5fdGltZXMgPSAyNSkgCmBgYAoKKiBIZXJlLCB3ZSBjYW4gZnVydGhlciBwcmVwcm9jZXNzIHRvIGRvIHdoYXRldmVyIHdlIHdvdWxkIGxpa2UsIHN1Y2ggYXMgb2J0YWluaW5nIGEgZHRtCgpgYGB7cn0KcmVjaXBlX2Jhc2UgJT4lIAogIHN0ZXBfdGYodGV4dCkgJT4lIAogIHByZXAoKSAlPiUgCiAganVpY2UoKSAlPiUgCiAgaGVhZCgxMDApCmBgYAoKYGBge3J9CnRleHRfcGNhIDwtIHRleHRfZHRtICU+JSAKICBjb2x1bW5fdG9fcm93bmFtZXMoJ2lkJykgJT4lIAogIHByY29tcChjZW50ZXIgPSBUUlVFLCBzY2FsZS4gPSBUUlVFLCByYW5rLiA9IDEwKQpgYGAKCmBgYHtyfQp0ZXh0X3BjYSAlPiUgZ2xpbXBzZSgpCmBgYAoKYGBge3J9CnRleHRfcGNhW1sneCddXSAlPiUKICBoZWFkKCkKYGBgCgoqIEFnYWluLCBhbHRlcm5hdGl2ZWx5IHdpdGggYSByZWNpcGUuLi4KCmBgYHtyfQpyZWNpcGVfcGNhIDwtIHJlY2lwZV9iYXNlICU+JSAjIHRva2VuaXplCiAgc3RlcF90ZmlkZih0ZXh0LCBwcmVmaXggPSAnJykgJT4lICMgVEZJREYgd2VpZ2h0aW5nCiAgc3RlcF9wY2EoYWxsX3ByZWRpY3RvcnMoKSwgbnVtX2NvbXAgPSAxMCkgJT4lICMgUENBCiAgcHJlcCgpIApgYGAKCmBgYHtyfQpyZWNpcGVfcGNhICU+JSBqdWljZSgpCmBgYAoqIFNvbWUgcGxvdHRpbmcKCmBgYHtyfQpyZWNpcGVfcGNhICU+JSBqdWljZSgpICU+JQogIGdncGxvdChhZXMoeCA9IFBDMDEsIHkgPSBQQzAyKSkgKwogIGdlb21fcG9pbnQoKSAKYGBgCiogd2UgY2FuIGFsc28gdXNlIHRoZSB0aWR5IHJlc3VsdHMgb2YgdGhlIHJlY2lwZSB0byBkbyBzb21lIG1vcmUgYW5hbHl0aWNzCgpgYGB7cn0KcmVjaXBlX3BjYSAlPiUKICB0aWR5KDcpICU+JQogIGZpbHRlcihjb21wb25lbnQgJWluJSBwYXN0ZTAoIlBDIiwgMTo0KSkgJT4lCiAgZ3JvdXBfYnkoY29tcG9uZW50KSAlPiUKICAgIGFycmFuZ2UoZGVzYyh2YWx1ZSkpICU+JQogICAgc2xpY2UoYygxOjIsIChuKCktMik6bigpKSkgJT4lCiAgdW5ncm91cCgpICU+JQogIG11dGF0ZShjb21wb25lbnQgPSBmY3RfaW5vcmRlcihjb21wb25lbnQpKSAlPiUKICBnZ3Bsb3QoYWVzKHZhbHVlLCB0ZXJtcywgZmlsbCA9IHRlcm1zKSkgKwogIGdlb21fY29sKHNob3cubGVnZW5kID0gRkFMU0UpICsKICBmYWNldF93cmFwKH5jb21wb25lbnQsIG5yb3cgPSAxKSArCiAgbGFicyh5ID0gTlVMTCkKYGBgCgoqICoqTm90ZSoqOiBBbHNvIGNoZWNrIGZ1cnRoZXIgZm9yIGZ1cnRoZXIgZGltZW5zaW9ubGl0eSByZWR1Y3Rpb24gc3RlcHM6CiAgICogdGVwX2twY2EoKToKICAgKiBzdGVwX2ljYSgpCiAgICogc3RlcF9pc29tYXAoKQogICAqIHN0ZXBfbm1mKCkKCgojIFRvcGljIE1vZGVsczogTGF0ZW50LURpcmljaGxldC1BbGxvY2F0aW9uIChMREEpCgoqIFdoaWxlIHdlIGFscmVhZHkgZGlkIGl0IHNvbWV3aGF0ICdvbi10aGUtZmx5JywgaGVyZSBhIG1vcmUgZm9ybWFsIGludHJvZHVjdGlvbiB0byBMREEKKiBJbiBjb250cmFzdCB0byBkaW1uZXNpb25hbGl0eSByZWR1Y3Rpb24gdGVjaGlxdWVzIG1vc3RseSBhaW1pbmcgYXQgcHJlcHJvY2Vzc2luZyBkYXRhIG9yIGVhc2luZyB2aXN1YWxpemF0aW9uLCBMREEgbW9yZSBhaW1zIGF0IEVEQSBhbmQgaW50ZXJwcmV0YXRpb24KKiBJdCBpcyBhIGdlbmVyYXRpdmUgYXBwcm9hY2ggdG8gaWRlbnRpZnkgdG9waWNzIChjbHVzdGVycykgd2l0aGluIHRoZSB3b3JkLXVzYWdlIGluIGRvY3VtZW50cy4KICAgKiBUb3BpY3MgYXJlIHJlcHJlc2VudGVkIGFzIGEgcHJvYmFiaWxpdHkgZGlzdHJpYnV0aW9uIG92ZXIgdGhlIHdvcmRzIGluIHRoZSB2b2NhYnVsYXJ5LiBIaGlnaCBwcm9iYWJpbGl0eSB3b3JkcyBjYW4gYmUgdXNlZCB0byBjaGFyYWN0cml6ZSB0aGUgdG9waWMuCiAgICogRG9jdW1lbnRzIGFyZSByZXByZXNlbnRlZCBhcyBhIG1peHR1cmUgb2YgdG9waWNzLgoKIVthbHQgdGV4dF0oaHR0cHM6Ly9taXJvLm1lZGl1bS5jb20vbWF4LzE2MDAvMSpwWm9fSWN4VzFHVnVIMnZRS2RvSU1RLmpwZWcpCgoKYGBge3J9CmxpYnJhcnkodG9waWNtb2RlbHMpCmBgYAoKCmBgYHtyfQp0ZXh0X2R0bSA8LSB0ZXh0X3RpZHkgJT4lCiAgY2FzdF9kdG0oZG9jdW1lbnQgPSBpZCwgdGVybSA9IHdvcmQsIHZhbHVlID0gbikKYGBgCgpgYGB7cn0KdGV4dF9sZGEgPC0gdGV4dF9kdG0gJT4lIAogIExEQShrID0gNiwgbWV0aG9kID0gIkdpYmJzIiwKICAgICAgY29udHJvbCA9IGxpc3Qoc2VlZCA9IDEzMzcpKQpgYGAKCgoqICRcYmV0YSQgaXMgYW4gb3V0cHV0IG9mIHRoZSBMREEgbW9kZWwsIGluZGljYXRpbmcgdGhlIHByb3BhYmlsaXR5IHRoYXQgYSB3b3JkIG9jY3VycyBpbiBhIGNlcnRhaW4gdG9waWMuCiogVGhlcmVmb3JlLCBsb2tpbmcgYXQgdGhlIHRvcCBwcm9iYWJpbGl0eSB3b3JkcyBvZiBhIHRvcGljIG9mdGVuIGdpdmVzIHVzIGEgZ29vZCBpbnR1aXRpb24gcmVnYXJkaW5nIGl0cyBwcm9wZXJ0aWVzLgoKYGBge3J9CiMgTERBIG91dHB1dCBpcyBkZWZpbmVkIGZvciB0aWR5KCksIHNvIHdlIGNhbiBlYXNpbHkgZXh0cmFjdCBpdApsZGFfYmV0YSA8LSB0ZXh0X2xkYSAlPiUgCiAgdGlkeShtYXRyaXggPSAiYmV0YSIpIApgYGAKCmBgYHtyfQpsZGFfYmV0YSAlPiUKICAjIHNsaWNlCiAgZ3JvdXBfYnkodG9waWMpICU+JQogIGFycmFuZ2UodG9waWMsIGRlc2MoYmV0YSkpICU+JQogIHNsaWNlKDE6MTApICU+JQogIHVuZ3JvdXAoKSAlPiUKICAjIHZpc3VhbGl6ZQogIG11dGF0ZSh0ZXJtID0gcmVvcmRlcl93aXRoaW4odGVybSwgYmV0YSwgdG9waWMpKSAlPiUKICBncm91cF9ieSh0b3BpYywgdGVybSkgJT4lICAgIAogIGFycmFuZ2UoZGVzYyhiZXRhKSkgJT4lICAKICB1bmdyb3VwKCkgJT4lCiAgZ2dwbG90KGFlcyh0ZXJtLCBiZXRhLCBmaWxsID0gYXMuZmFjdG9yKHRvcGljKSkpICsKICBnZW9tX2NvbChzaG93LmxlZ2VuZCA9IEZBTFNFKSArCiAgY29vcmRfZmxpcCgpICsKICBzY2FsZV94X3Jlb3JkZXJlZCgpICsKICBsYWJzKHRpdGxlID0gIlRvcCAxMCB0ZXJtcyBpbiBlYWNoIExEQSB0b3BpYyIsCiAgICAgICB4ID0gTlVMTCwgeSA9IGV4cHJlc3Npb24oYmV0YSkpICsKICBmYWNldF93cmFwKH4gdG9waWMsIG5jb2wgPSAzLCBzY2FsZXMgPSAiZnJlZSIpCmBgYAoKKiBEb2N1bWVudHMgYXJlIHJlcHJlc2VudGVkIGFzIGEgbWl4IG9mIHRvcGljcy4gVGhpcyBhc3NvY2lhdGlvbiBvZiBhIGRvY3VtZW50IHRvIGEgdG9waWMgaXMgY2FwdHVyZWQgYnkgJFxnYW1tYSQKCmBgYHtyfQpsZGFfZ2FtbWEgPC0gdGV4dF9sZGEgJT4lIAogIHRpZHkobWF0cml4ID0gImdhbW1hIikKYGBgCgoKYGBge3J9CmxkYV9nYW1tYSAlPiUKICBncm91cF9ieSh0b3BpYykgJT4lCiAgICBhcnJhbmdlKGRlc2MoZ2FtbWEpKSAlPiUgCiAgICBzbGljZSgxOjEwKSAlPiUKICB1bmdyb3VwKCkgJT4lCiAgbGVmdF9qb2luKHRleHQgJT4lIHNlbGVjdChpZCwgcHJvamVjdGFjcm9ueW0pICU+JSBtdXRhdGUoaWQgPSBpZCAlPiUgYXMuY2hhcmFjdGVyKCkpLCBieSA9IGMoJ2RvY3VtZW50JyA9ICdpZCcpKQpgYGAKCgoKKiBOb3RlIHRoYXQgYW4gTERBIGNhbiBhbHNvIGJlIHBlcmZvcm1lZCB2aWEgYSByZWNpcGU6CgpgYGB7cn0KcmVjaXBlX2xkYSA8LSByZWNpcGVfYmFzZSAlPiUgIyB0b2tlbml6ZQogIHN0ZXBfdW50b2tlbml6ZSh0ZXh0KSAlPiUgIyBJcyBhIGJpdCBzaWxseSwgbmVlZHMgdGhlIGZ1bGwgdGV4dCB2ZWN0b3JzIGluc3RlYWQgb2YgdG9rZW5zLi4uLgogIHN0ZXBfbGRhKHRleHQsIG51bV90b3BpY3MgPSA2KSAlPiUgIyBMREEKICBwcmVwKCkgCmBgYAoKYGBge3J9CnJlY2lwZV9sZGEgJT4lIGp1aWNlKCkgJT4lIAogIGhlYWQoMTAwKQpgYGAKCiogQXMgYSBib251cywgYSBncmVhdCB3YXkgdG8gaW50ZXJhY3RpdmVseSB2aXN1YWxpemUgTERBJ3MuCiogSXQncyBhIGJpdCBjdW1iZXJzb21lIGluIFIsIHRob3VnaC4uLgoKYGBge3J9CmxpYnJhcnkoTERBdmlzKQpgYGAKCgpgYGB7cn0KIyBBIGJpdCBvZiBhIGxlbmdodHkgZnVuY3Rpb24uLi4uCnRvcGljbW9kZWxzX2pzb25fbGRhdmlzIDwtIGZ1bmN0aW9uKGZpdHRlZCwgZG9jX2R0bSwgbWV0aG9kID0gIlBDQSIpewogIHJlcXVpcmUodG9waWNtb2RlbHMpOyByZXF1aXJlKGRwbHlyKTsgcmVxdWlyZShMREF2aXMpCiAgCiAgIyBGaW5kIHJlcXVpcmVkIHF1YW50aXRpZXMKICBwaGkgPC0gcG9zdGVyaW9yKHRleHRfbGRhKSR0ZXJtcyAlPiUgYXMubWF0cml4KCkgIyBUb3BpYy10ZXJtIGRpc3RyaWJ1dGlvbgogIHRoZXRhIDwtIHBvc3RlcmlvcihmaXR0ZWQpJHRvcGljcyAlPiUgYXMubWF0cml4KCkgIyBEb2N1bWVudC10b3BpYyBtYXRyaXgKICAKICB0ZXh0X3RpZHkgPC0gZG9jX2R0bSAlPiUgdGlkeSgpCiAgdm9jYWIgPC0gY29sbmFtZXMocGhpKQogIGRvY19sZW5ndGggPC0gdGliYmxlKGRvY3VtZW50ID0gcm93bmFtZXModGhldGEpKSAlPiUgbGVmdF9qb2luKHRleHRfdGlkeSAlPiUgY291bnQoZG9jdW1lbnQsIHd0ID0gY291bnQpLCBieSA9ICdkb2N1bWVudCcpCiAgdGYgPC0gdGliYmxlKHRlcm0gPSB2b2NhYikgJT4lIGxlZnRfam9pbih0ZXh0X3RpZHkgJT4lIGNvdW50KHRlcm0sIHd0ID0gY291bnQpLCBieSA9ICJ0ZXJtIikgCiAgCiAgaWYobWV0aG9kID09ICJQQ0EiKXttZHMgPC0ganNQQ0F9CiAgaWYobWV0aG9kID09ICJUU05FIil7bGlicmFyeSh0c25lKTsgbWRzIDwtIGZ1bmN0aW9uKHgpe3RzbmUoc3ZkKHgpJHUpfSB9CiAgCiAgIyBDb252ZXJ0IHRvIGpzb24KICBqc29uX2xkYSA8LSBMREF2aXM6OmNyZWF0ZUpTT04ocGhpID0gcGhpLCB0aGV0YSA9IHRoZXRhLCB2b2NhYiA9IHZvY2FiLCBkb2MubGVuZ3RoID0gZG9jX2xlbmd0aCAlPiUgcHVsbChuKSwgdGVybS5mcmVxdWVuY3kgPSB0ZiAlPiUgcHVsbChuKSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmVvcmRlci50b3BpY3MgPSBGQUxTRSwgbWRzLm1ldGhvZCA9IG1kcyxwbG90Lm9wdHMgPSBsaXN0KHhsYWIgPSAiRGltLjEiLCB5bGFiID0gIkRpbS4yIikpIAogIHJldHVybihqc29uX2xkYSkKfQpgYGAKCgpgYGB7cn0KbGlicmFyeShMREF2aXMpCmpzb25fbGRhIDwtIHRvcGljbW9kZWxzX2pzb25fbGRhdmlzKGZpdHRlZCA9IHRleHRfbGRhLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZG9jX2R0bSA9IHRleHRfZHRtLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbWV0aG9kID0gIlRTTkUiKQoKIyBqc29uX2xkYSAlPiUgc2VyVmlzKCkgIyBGb3IgZGlyZWN0IG91dHB1dAojIGpzb25fbGRhICU+JSBzZXJWaXMob3V0LmRpciA9ICdMREF2aXonKSAjIEZvciBzYXZpbmcgdGhlIGh0bWwKYGBgCgoKPCEtLSA8aWZyYW1lIHdpZHRoPSIxMjAwIiBoZWlnaHQ9IjE1MDAiIHNyYz0iaHR0cHM6Ly9zZHMtYWF1LmdpdGh1Yi5pby9TRFMtbWFzdGVyL00yL25vdGVib29rcy9MREF2aXovaW5kZXguaHRtbCIgZnJhbWVib3JkZXI9IjAiPjwvaWZyYW1lPiAtLT4KCkRpZG50IHJlYWxseSBmaWd1cmUgb3V0IGhvdyB0byBlbWJlZGQgdGhlIHJlc3VsdGluZyBwbG90LCBidXQgdGhlIG91dGNvbWUgY2FuIGJlIHNlZW4gW2hlcmVdKGh0dHBzOi8vc2RzLWFhdS5naXRodWIuaW8vU0RTLW1hc3Rlci9NMi9ub3RlYm9va3MvTERBdml6L2luZGV4Lmh0bWwpCgoKIyBFbWJlZGRpbmdzIChCb251cykKCiogT25lIGxhc3QgdGhpbmcgd2UgZGlkIG5vdCB2ZW50dXJlIGluIHlldCwgYXJlIGVtYmVkZGluZ3MKKiBJIHdpbGwgbm90IGdvIGludG8gZGV0YWlscyBoZXJlLCBqdXN0IHNlZSBpdCBhcyBhIHBlYWsgb2Ygd2hhdCdzIHRvIGNvbWUgaW4gZnVydGhlciBzZXNzaW9ucy4KKiBUaGUgaWRlZSBvZiB3b3JkIGVtYmVkZGluZyBpcyAoaW4gYSBudXRzaGVsbCkgdGhhdAoKCiogVGhlcmUgYXJlIHBhY2thZ2VzIG9uIGhvdyB0byB0cmFpbiBvd24gZW1iZWRkaW5ncyBzdWNoIGFzIFtgdGV4dDJ2ZWNgXShodHRwOi8vdGV4dDJ2ZWMub3JnLyksIGJ1dCB3ZSB3aWxsIGZvciBub3cgbm90IGJvdGhlciB3aXRoIHRoYXQuCiogVGhlIG9ubHkgdGhpbmcgd2Ugd2lsbCBkbyBmb3Igbm93IGlzIHRvIGxvYWQgcHJldHJhaW5lZCBlbWJlZGRpbmdzIChHbG9WZSwgY2YuIFBlbm5pbmd0b24gZXQgYWwsIDIwMTQpCgoKYGBge3J9CmxpYnJhcnkodGV4dGRhdGEpCgpnbG92ZTZiIDwtIGVtYmVkZGluZ19nbG92ZTZiKGRpbWVuc2lvbnMgPSAxMDApCmdsb3ZlNmIKYGBgCgoKKiBMYSB2b2lsYSwgYSBsYXJnZSBwcmV0cmFpbmVkIGVtYmVkZGluZyBtb2RlbCBmb3IgYXJvdW5kIDQwMGsgb2YgdGhlIG1vc3QgY29tbW9uIHdvcmRzLiAKKiBXZSBmb3Igbm93IGxvYWRlZCB0aGUgc21hbGxlc3Qgb2YgdGhlc2UgZW1iZWRkaW5nIG1vZGVscywgdGhlcmUgZXhpc3Qgd2F5IGJpZ2dlciBvbmVzLgoqIExldHMgam9pbiBpdCB3aXRoIG91ciB0aWR5IHRva2VubGlzdAoKYGBge3J9CndvcmRfZW1iZWRkaW5ncyA8LSB0ZXh0X3RpZHkgJT4lCiAgaW5uZXJfam9pbihnbG92ZTZiLCBieSA9IGMoJ3dvcmQnID0gJ3Rva2VuJykpCmBgYAoKYGBge3J9CndvcmRfZW1iZWRkaW5ncyAlPiUgaGVhZCgpCmBgYAoKKiBXZSBjb3VsZCBub3cgY3JlYXRlIGF2ZXJhZ2UgZG9jdW1lbnQgZW1iZWRkaW5ncyBieSB0YWtpbmcgdGhlIG1lYW4gb3ZlciBhbGwgZGltZW5zaW9ucwoqIFdlIGNvdWxkIGFsc28gKGV2ZW4gYmV0dGVyKSB3ZWlnaHQgdGhhdCBieSB0aGVuIHdvcmQncyB0ZmlkZiBzY29yZS4KCmBgYHtyfQpkb2NfZW1iZWRkaW5ncyA8LSB3b3JkX2VtYmVkZGluZ3MgJT4lCiAgZ3JvdXBfYnkoaWQpICU+JQogIHN1bW1hcmlzZShhY3Jvc3Moc3RhcnRzX3dpdGgoImQiKSwgfm1lYW4oLnggLyB0Zl9pZGYsIG5hLnJtID0gVFJVRSkpKQpgYGAKCiogVGhlc2UgZW1iZGRpbmdzIGNvdWxkIG5vdyBiZSB1c2VkIGZvciBpbnN0YW5jZSBmb3Igc29tZSBjbHVzdGVyaW5nIG9yIFNNTCBleGVyY2lzZQoqIEkgZ3Vlc3MgeW91IGNhbiBhbHJlYWR5IHNlZSBob3cgdG8gdXNlIHRoZXNlIGVtYmVkZGluZ3MgaW4gYW4gU01MIG1vZGVsLgoKCmBgYHtyfQpsaWJyYXJ5KHV3b3QpICMgZm9yIFVNQVAKYGBgCgoKYGBge3J9CmVtYmVkZGluZ3NfdW1hcCA8LSBkb2NfZW1iZWRkaW5ncyAgJT4lIAogIGNvbHVtbl90b19yb3duYW1lcygiaWQiKSAlPiUKICB1bWFwKG5fbmVpZ2hib3JzID0gMTUsIAogICAgICAgbWV0cmljID0gImNvc2luZSIsIAogICAgICAgbWluX2Rpc3QgPSAwLjAxLCAKICAgICAgIHNjYWxlID0gVFJVRSwKICAgICAgIHZlcmJvc2UgPSBUUlVFLCAKICAgICAgIG5fdGhyZWFkcyA9IDgpIApgYGAKCmBgYHtyfQplbWJlZGRpbmdzX3VtYXAgJTw+JSBhcy5kYXRhLmZyYW1lKCkKYGBgCgoKYGBge3J9CmVtYmVkZGluZ3NfdW1hcCAgJT4lIAogIGdncGxvdChhZXMoeCA9IFYxLCB5ID0gVjIpKSArIAogIGdlb21fcG9pbnQoc2hhcGUgPSAyMSwgYWxwaGEgPSAwLjUpIApgYGAKCiogT2ssIHdlIHNlZSBhIHJhdGhlciBjbGVhciBzZXBlcmF0aW9uIG9mIGRvY3VtZW50cy4KKiBKdXN0IGZvciBmdW4sIGxldHMgYWRkIGEgZGVuc2l0eSBiYXNlZCBjbHVzdGVyaW5nICh2ZXJ5IGdvb2QgZm9yIHNwYXRpYWwgY2x1c3RlcmluZykgb24gdG9wIChldmVuIHRob3VnaCB3ZSBhbHJlYWR5IHNlZSB0aGUgcmVzdWx0cykKCmBgYHtyfQpsaWJyYXJ5KGRic2NhbikKYGBgCgoqIERvIHRoZSBoaXJhcmNoaWNhbCBkZW5zaXR5IGJhc2VkIGNsdXN0ZXJpbmcKICAgICAgIApgYGB7cn0KZW1iZWRkaW5nc19oZGJzY2FuIDwtIGVtYmVkZGluZ3NfdW1hcCAlPiUgYXMubWF0cml4KCkgJT4lIGhkYnNjYW4obWluUHRzID0gMTUpCmBgYAoKKiBQbG90IGl0CgpgYGB7cn0KZW1iZWRkaW5nc191bWFwICU+JSAKICBiaW5kX2NvbHMoY2x1c3RlciA9IGVtYmVkZGluZ3NfaGRic2NhbiRjbHVzdGVyICU+JSBhcy5mYWN0b3IoKSwgCiAgICAgICAgICAgIHByb2IgPSBlbWJlZGRpbmdzX2hkYnNjYW4kbWVtYmVyc2hpcF9wcm9iKSAlPiUKICBnZ3Bsb3QoYWVzKHggPSBWMSwgeSA9IFYyLCBjb2wgPSBjbHVzdGVyKSkgKyAKICBnZW9tX3BvaW50KGFlcyhhbHBoYSA9IHByb2IpLCBzaGFwZSA9IDIxKSAKYGBgCgoqIE5vdGU6IFdlIGNhbiBhbHNvIGFzc2lnbmUgdGhlIGVtYmVkZGluZ3MgdmlhIGEgcmVjaXBlCiogVW5mb3J0dW5hdGVseSwgd2UgY2FuIG5vdCBkbyBhIFRGSURGIHdlaWdodGluZyBoZXJlICdvdXQtb2YtdGhlLWJveCcsIGJ1dCBoYXZlIHRvIHdvcmsgd2l0aCBhdmVyYWdlIGVtYmVkZGluZ3MgaW5zdGVhZC4KCgpgYGB7cn0KcmVjaXBlX2VtYmVkZGluZyA8LSByZWNpcGVfYmFzZSAlPiUgIyB0b2tlbml6ZQogIHN0ZXBfd29yZF9lbWJlZGRpbmdzKHRleHQsIGVtYmVkZGluZ3MgPSBnbG92ZTZiLCBhZ2dyZWdhdGlvbiA9ICdtZWFuJykKYGBgCgpgYGB7cn0KcmVjaXBlX2VtYmVkZGluZyAlPiUgcHJlcCgpICU+JSBqdWljZSgpICU+JSAKICBoZWFkKDEwMCkKYGBgCgoKKiBTYW1lIGdvZXMgZm9yIFVNQVAsIHdoaWNoIGNhbiBiZSBhY2Nlc3NkIGluIHJlY2lwZXMgdmlhIHRoZSB0aGUgcGFja2FnZSBgZW1iZWRgIHBja2FnZS4KKiBIb3dldmVyLGBlbWJlZGAgaXMgYSBiaXQgaGVhdnkgaW4gdGVybXMgb2YgZGVwZW5kZW5jaWVzLCBzaW5jZSBpdCB1c2VzIGBrZXJhc2AgYW5kIGB0ZW5zb3JmbG93YCwgYSBkZWVwIGxlYXJuaW5nIGZyYW1ld29rLCBpbiB0aGUgYmFja2dyb3VibmQsIGFuZCBpcyBpbiBuZWVkIHRvIGluc3RhbGwgYW5vdGhlciBtaW5pLWNvbmRhIGVudmlyb21lbnQuIAoqIElmIHlvdSBoYXZlIG5vIGV4cGVyaWVuY2Ugd2l0aCBga2VyYXNgIGFuZCBgdGVuc29yZmxvd2Agc28gZmFyLCBJIHN1Z2dlc3QgeW91IHdhaXQgd2l0aCB0aGlzIG9uZSB1bnRpbCBsYXRlciBzZXNzaW9ucyB3aGVuIHdlIHByb3Blcmx5IGludHJvZHVjZSBpdC4KCmBgYHtyfQpsaWJyYXJ5KGVtYmVkKQpgYGAKCmBgYHtyfQpyZWNpcGVfdW1hcCA8LSByZWNpcGVfZW1iZWRkaW5nICU+JQogIHN0ZXBfdW1hcChzdGFydHNfd2l0aCgnd19lbWJlZCcpLCBuX25laWdoYm9ycyA9IDE1KSAKYGBgCgpgYGB7cn0KcmVjaXBlX3VtYXAgJT4lIHByZXAoKSAlPiUganVpY2UoKSAlPiUgCiAgaGVhZCgxMDApCmBgYAoKKiBTbywgdGhhdCdzIGFsbCBJIGhhdmUgZm9yIG5vdwoKIyBTdW1tYXJ5CgoqIFRoZXJlIGFyZSBtYW55IHdheXMgdG8gY29udmVydCB0ZXh0IGRhdGEgaW50byBhIHZlY3RvciByZXByZXNlbnRhdGlvbi4KKiBUaGVzZSByYW5nZSBmcm9tIHNpbXBsZSBhbmQgd2VpZ2h0ZWQgYmFncy1vZi13b3JkcywgdG8gdG9waWMgbW9kZWxzLCBvdmVyIGRpZmZlcmVudCB0eXBlcyBvZiBkaW1lbnNpb25hbGl0eSByZWR1Y3Rpb24gdG8gZmluYWxseSB3b3JkIGFuZCBkb2N1bWVudCBlbWJlZGRpbmdzLgoqIEFsbCBvZiB0aGVtIGFyZSB1c2VmdWwsIGRlcGVuZGluZyBvbiB0aGUgcHVycG9zZS4KCiMgRW5kbm90ZXMKCiMjIyBQYWNrYWdlcyAmIEVjb3N5c3RlbQoKKiBbYHRleHRyZWNpcGVzYF0oaHR0cHM6Ly90ZXh0cmVjaXBlcy50aWR5bW9kZWxzLm9yZy8pOiBUZXh0IHByZXByb2Nlc3NpbmcgcmVjaXBlcwoqIFtgZW1iZWRgXShodHRwczovL2VtYmVkLnRpZHltb2RlbHMub3JnLyk6IEV4dHJhIGVtYmVkZGluZyByZWNpcGVzCiogW2B0b3BpY21vZGVsc2BdKGh0dHBzOi8vY3Jhbi5yLXByb2plY3Qub3JnL3dlYi9wYWNrYWdlcy90b3BpY21vZGVscy92aWduZXR0ZXMvdG9waWNtb2RlbHMucGRmKTogTERBIHRvcGljbW9kZWxsaW5nIGluIFIKKiBbYExEQXZpc2BdKGh0dHBzOi8vZ2l0aHViLmNvbS9jcHNpZXZlcnQvTERBdml6KTogQSBiaXQgY2x1bmt5IGJ1dCBhd2Vzb21lIGludGVyYWN0aXZlIExEQSB2aXN1YWxpemF0aW9ucwoqIFtgdGV4dDJ2ZWNgXShodHRwOi8vdGV4dDJ2ZWMub3JnLyk6IFBhY2thZ2Ugdm9yIHZlY3RvciBzcGFjZSBtb2RlbGxpbmcgKGFrYSBlbWJlZGRpbmdzICYgb3RoZXIgdmVjdG9yaXphdGlvbnMpIG9mIHRleHRkYXRhCiogW2B0ZXh0ZGF0YWBdKGh0dHBzOi8vZ2l0aHViLmNvbS9FbWlsSHZpdGZlbGR0L3RleHRkYXRhKTogVXNlZnVsIGRhdGFzZXRzIGZvciB0ZXh0LCBzdWNoIGFzIEdsb1ZlIGVtYmVkZGluZ3MsIHNlbnRpbWVudCBsZXhpY2EgZXRjLgoqIFtgdXdvdGBdKGh0dHBzOi8vZ2l0aHViLmNvbS9qbG1lbHZpbGxlL3V3b3QpOiBVTUFQIGZvciBSCiogW2B1d290YF0oaHR0cHM6Ly9naXRodWIuY29tL2psbWVsdmlsbGUvdXdvdCk6IFVNQVAgZm9yIFIKCiMjIyBSZWZlcmVuY2VzIAoKQ0hhcHRlcnM6CgoqIEp1bGlhIFNpbGdlIGFuZCBEYXZpZCBSb2JpbnNvbiAoMjAyMCkuIFRleHQgTWluaW5nIHdpdGggUjogQSBUaWR5IEFwcHJvYWNoLCBP4oCZUmVpbGx5LiBPbmxpbmUgYXZhaWxhYmxlIFtoZXJlXShodHRwczovL3d3dy50aWR5dGV4dG1pbmluZy5jb20vKQogICAqIFtDaGFwdGVyIDZdKGh0dHBzOi8vd3d3LnRpZHl0ZXh0bWluaW5nLmNvbS90b3BpY21vZGVsaW5nLmh0bWwpOiB4eHgKKiBFbWlsIEh2aWRmZWxkdCBhbmQgSnVsaWEgU2lsZ2UgKDIwMjApLiBTdXBlcnZpc2VkIE1hY2hpbmUgTGVhcm5pbmcgZm9yIFRleHQgQW5hbHlzaXMgaW4gUiwgb25saW5lIGF2YWlsYWJsZSBbaGVyZV0oaHR0cHM6Ly9zbWx0YXIuY29tLykKICAgKiBbQ2hhcHRlciA1XShodHRwczovL3NtbHRhci5jb20vZW1iZWRkaW5ncy5odG1sKTogV29yZCBFbWJlZGRpbmdzCgoKQXJ0aWNsZXM6CiogQmxlaSwgRGF2aWQgTS4sIEFuZHJldyBZLiBOZywgYW5kIE1pY2hhZWwgSS4gSm9yZGFuLiAiTGF0ZW50IGRpcmljaGxldCBhbGxvY2F0aW9uLiIgSm91cm5hbCBvZiBtYWNoaW5lIExlYXJuaW5nIHJlc2VhcmNoIDMsIG5vLiBKYW4gKDIwMDMpOiA5OTMtMTAyMi4KKiBKZWZmcmV5IFBlbm5pbmd0b24sIFJpY2hhcmQgU29jaGVyLCBhbmQgQ2hyaXN0b3BoZXIgRCBNYW5uaW5nLiBHbG92ZTogR2xvYmFsIHZlY3RvcnMgZm9yIHdvcmQgcmVwcmVzZW50YXRpb24uIEluIENvbmZlcmVuY2Ugb24gRW1waXJpY2FsIE1ldGhvZHMgb24gTmF0dXJhbCBMYW5ndWFnZSBQcm9jZXNzaW5nIChFTU5MUCksIHBhZ2VzIDE1MzLigJMxNTQzLCAyMDE0CgojIyMgRnVydGhlciBzb3VyY2VzCgoKKiBbSnVsaWEgU2lsZ2UncyBCbG9nXShodHRwczovL2p1bGlhc2lsZ2UuY29tLyk6IEZ1bGwgb2YgZ3JlYXQgZXhhbXBsZXMgb2YgcHJlZGljdGl2ZSBtb2RlbGluZywgTkxQLCBhbmQgdGhlIGNvbWJpbmF0aW9uIGZvIGJvdGgsIHVzaW5nIHRpZHkgZWNvc3lzdGVtcwoqIFtFbWlsIEh2aXRmZWxkdCdzIEJsb2ddKGh0dHBzOi8vd3d3Lmh2aXRmZWxkdC5tZS8pOiBMaWtld2lzZSwgZnVsbCBvZiBncmVhdCBleGFtcGxlcyBvZiBhcHBsaWVkIHRpZHkgTUwgJiBOTFAgaW4gUgoKIyMjIFNlc3Npb24gSW5mbwoKYGBge3J9CnNlc3Npb25JbmZvKCkKYGBgCgoKCgoKCg==